package com.moonshineproject.plugin.mxmlc
{
	import com.moonshineproject.IDEModel;
	import com.moonshineproject.plugin.IMenuPlugin;
	import com.moonshineproject.plugin.IPlugin;
	import com.moonshineproject.plugin.PluginBase;
	import com.moonshineproject.plugin.console.MarkupTextLineModel;
	import com.moonshineproject.plugin.menu.vo.MenuItem;
	import com.moonshineproject.plugin.project.vo.ProjectVO;
	import com.moonshineproject.plugin.project.vo.flash.AS3ProjectVO;
	import com.moonshineproject.plugin.settings.ISettingsProvider;
	import com.moonshineproject.plugin.settings.vo.BooleanSetting;
	import com.moonshineproject.plugin.settings.vo.ISetting;
	import com.moonshineproject.plugin.settings.vo.PathSetting;
	import com.moonshineproject.text.TextLineModel;
	import com.moonshineproject.utils.HtmlFormatter;
	import com.moonshineproject.vo.Settings;
	
	import flash.desktop.NativeProcess;
	import flash.desktop.NativeProcessStartupInfo;
	import flash.events.Event;
	import flash.events.NativeProcessExitEvent;
	import flash.events.ProgressEvent;
	import flash.filesystem.File;
	import flash.ui.Keyboard;
	import flash.utils.Dictionary;
	import flash.utils.IDataInput;
	import flash.utils.IDataOutput;
	
	import no.doomsday.console.ConsoleUtil;

	public class MXMLCPlugin extends PluginBase implements IPlugin, ISettingsProvider, IMenuPlugin
	{
		override public function get name():String			{ return "MXMLC Compiler Plugin"; }
		override public function get author():String		{ return "Miha Lunar & Moonshine Project Team"; }
		override public function get description():String	{ return "Compiles AS3 projects with MXMLC."; }
		
		
		public var defaultFlexSDK:String
		public var incrementalCompile:Boolean = false;
		protected var runAfterBuild:Boolean;
				
		private var fcshPath:String = "bin/fcsh";

		public function get flexSDK():File
		{
			return currentSDK;
		}

		private var fcsh:NativeProcess;
		private var exiting:Boolean = false;
		private var shellInfo:NativeProcessStartupInfo;
		
		private var lastTarget:File;
		private var targets:Dictionary;
		
		private var currentSDK:File = flexSDK;
		
		/** Project currently under compilation */
		private var currentProject:ProjectVO;
		private var queue:Vector.<String> = new Vector.<String>();
		private var errors:String = "";
		
		private var cmdLine:CommandLine;
		private var _instance:MXMLCPlugin
		
		public function MXMLCPlugin() 
		{
			if (Settings.os == "win") fcshPath += ".exe";
		}
		
		override public function activate():void 
		{
			super.activate();
			
			dispatcher.addEventListener(MXMLCPluginEvent.BUILD_AND_RUN, buildAndRun);
			dispatcher.addEventListener(MXMLCPluginEvent.BUILD, build);
			
			registerCommand('build', buildCommand);
			registerCommand('run', runCommand);
			
			// For debugging IDE
			ConsoleUtil.linkFunction("fcsh", send);
			
			// Get shell
			shellInfo = new NativeProcessStartupInfo();
			cmdLine = new CommandLine();
			
			reset();
			_activated = true;
		}
		
		override public function deactivate():void 
		{
			super.deactivate();
			
			reset();
			shellInfo = null;
			cmdLine = null;
			_activated = false;
			
		}
		
		public function getSettingsList():Vector.<ISetting>
		{
			return Vector.<ISetting>([
				new PathSetting(this,'defaultFlexSDK', 'Default Flex SDK', true),
				new BooleanSetting(this,'incrementalCompile', 'Incremental Compilation')
			])
		}
		
		public function getMenu():MenuItem
		{
			// Since plugin will be activated if needed we can return null to block menu
			if( !_activated ) return null;
			return new MenuItem(
					"MXMLC", 
					[
						new MenuItem("Build", null, MXMLCPluginEvent.BUILD,
							 'b', [Keyboard.COMMAND],
							 'b', [Keyboard.CONTROL]),
						new MenuItem("Build & Run", null, MXMLCPluginEvent.BUILD_AND_RUN, 
									 "\n", [Keyboard.COMMAND],
									 "\n", [Keyboard.CONTROL])
					]
			);
		}
		
		private function buildCommand(args:Array):void
		{
			build(null, false);
		}
		
		private function runCommand(args:Array):void
		{
			build(null, true);
		}
		
		private function reset():void 
		{
			fcsh = null;
			targets = new Dictionary();
		}
		
		private function buildAndRun(e:Event):void
		{
			build(e, true);	
		}
		
		private function build(e:Event, runAfterBuild:Boolean=false):void 
		{
			this.runAfterBuild = runAfterBuild;
			
			var pvo:ProjectVO = IDEModel.getInstance().activeProject;
			
			if (!pvo) 
			{
				error("There are no projects opened.");
				return;
			} 
			else if (!(pvo is AS3ProjectVO))
			{
				error("Project is not an AS3 Project.");
				return;
			}
			
			if (!fcsh || pvo.file.nativePath != shellInfo.workingDirectory.nativePath 
				|| usingInvalidSDK(pvo as AS3ProjectVO)) 
			{
				currentSDK = getCurrentSDK(pvo as AS3ProjectVO);
				if (!currentSDK)
				{
					error("No Flex SDK set. Check settings.");
					return;
				}
				var fsch:File = currentSDK.resolvePath(fcshPath);
				if (!fsch.exists)
				{
					error("Couldn't find FSCH binary. Check settings.");
					return;
				}
				shellInfo.executable = fsch;
				shellInfo.workingDirectory = pvo.file;
				initShell();
			}
			
			ConsoleUtil.print("SDKSDK "+currentSDK.nativePath);
			compile(pvo as AS3ProjectVO);
		}
		
		/**
		 * @return True if the current SDK matches the project SDK, false otherwise
		 */
		private function usingInvalidSDK(pvo:AS3ProjectVO):Boolean 
		{
			var customSDK:File = pvo.buildOptions.customSDK;
			if ((customSDK && (currentSDK.nativePath != customSDK.nativePath))
			|| (!customSDK && currentSDK.nativePath != flexSDK.nativePath)) 
			{
				return true;
			}
			
			return false;
		}
		
		private function getCurrentSDK(pvo:AS3ProjectVO):File 
		{
			var customSDK:File = pvo.buildOptions.customSDK;
			return customSDK ? customSDK : (defaultFlexSDK ? new File(defaultFlexSDK) : null);
		}
		
		private function compile(pvo:AS3ProjectVO):void 
		{
			clearOutput();
			dispatcher.dispatchEvent(new MXMLCPluginEvent(MXMLCPluginEvent.PREBUILD, currentSDK));
			print("Compiling "+pvo.projectName);
			
			currentProject = pvo;
			pvo.updateConfig();
			if (pvo.targets.length == 0) 
			{
				error("No targets found for compilation.");
				return;
			}
			var file:File = pvo.targets[0];
			if (targets[file] == undefined) 
			{
				lastTarget = file;
				
				var mxmlcStr:String =
					"mxmlc"
					+" -load-config+="+pvo.file.getRelativePath(pvo.config.file)
					+" -debug=true"
					+(pvo.buildOptions.additional.length > 0 ? " "+pvo.buildOptions.additional.replace("\n", " ") : "")
				;
				
				send(mxmlcStr);
			} 
			else 
			{
				var target:int = targets[file];
				send("compile "+target);
			}
		}
		
		private function send(msg:String):void 
		{
			ConsoleUtil.print("Send to mxmlc: "+msg);
			if (!fcsh) {
				queue.push(msg);
			} else {
				var input:IDataOutput = fcsh.standardInput;
				input.writeUTFBytes(msg+"\n");
			}
		}
		
		private function flush():void 
		{
			if (queue.length == 0) return;
			if (fcsh) {
				for (var i:int = 0; i < queue.length; i++) {
					send(queue[i]);
				}
				queue.length = 0;
			}
		}
		
		private function initShell():void 
		{
			if (fcsh) {
				fcsh.exit();
				exiting = true;
				reset();
			} else {
				startShell();
			}
		}
		
		private function startShell():void 
		{
			fcsh = new NativeProcess();
			
			fcsh.addEventListener(ProgressEvent.STANDARD_OUTPUT_DATA, shellData);
			fcsh.addEventListener(ProgressEvent.STANDARD_ERROR_DATA, shellError);
			fcsh.addEventListener(NativeProcessExitEvent.EXIT, shellExit);
			fcsh.start(shellInfo);
			
			flush();
		}
		
		private function shellData(e:ProgressEvent):void {
			//if (!fcsh) return;
			var output:IDataInput = fcsh.standardOutput;
			var data:String = output.readUTFBytes(output.bytesAvailable);
			
			var match:Array;
			
			match = data.match(/fcsh: Assigned (\d) as the compile target id/);
			if (match && lastTarget) {
				var target:int = int(match[1]);
				targets[lastTarget] = target;
				ConsoleUtil.print("FCSH TARGET "+target);
				lastTarget = null;
			}
			
			match = data.match(/(.*) \(\d+? bytes\)/);
			if (match) 
			{ // Successful compile
				var swfPath:String = match[1];
				var swfFile:File = new File(swfPath);
				if (runAfterBuild)
				{
					testMovie(swfFile);
				}
				print("Done");
				dispatcher.dispatchEvent(new MXMLCPluginEvent(MXMLCPluginEvent.POSTBUILD, currentSDK));
			}
			
			if (data == "(fcsh) ") 
			{
				if (errors != "") 
				{
					compilerError(errors);
					errors = "";
				}
			}
			
			if (data.charAt(data.length-1) == "\n") data = data.substr(0, data.length-1);
			ConsoleUtil.print(data);
		}
		
		private function testMovie(swfFile:File):void 
		{
			var pvo:AS3ProjectVO = currentProject as AS3ProjectVO;
			if (pvo.testMovie == AS3ProjectVO.TEST_MOVIE_CUSTOM) 
			{
				var customSplit:Vector.<String> = Vector.<String>(pvo.testMovieCommand.split(";"));
				var customFile:String = customSplit[0];
				var customArgs:String = customSplit.slice(1).join(" ").replace("$(ProjectName)", pvo.projectName).replace("$(CompilerPath)", currentSDK.nativePath);
				
				if (Settings.os != "win")
				{
					var customInfo:NativeProcessStartupInfo = new NativeProcessStartupInfo();
					customInfo.executable = pvo.file.resolvePath(customSplit[0]);
					customInfo.arguments = new Vector.<String>(1);
					customInfo.arguments[0] = "bin-debug/IDE-app.xml";
					customInfo.workingDirectory = pvo.file;
					
					ConsoleUtil.print("Custom process: "+customInfo.executable.nativePath);
					ConsoleUtil.print("Custom path: "+customInfo.workingDirectory.nativePath);
					ConsoleUtil.print("Custom args: "+customInfo.arguments.toString());
					var customProcess:NativeProcess = new NativeProcess();
					customProcess.start(customInfo);
				}
				else
				{
					cmdLine.write(customFile+" "+customArgs, pvo.file);
				}
			} else 
			{
				swfFile.openWithDefaultApplication();
			}
			currentProject = null;
		}
		private function shellError(e:ProgressEvent):void {
			//if (!fcsh) return;
			var output:IDataInput = fcsh.standardError;
			var data:String = output.readUTFBytes(output.bytesAvailable);
			
			var syntaxMatch:Array;
			var generalMatch:Array;
			
			syntaxMatch = data.match(/(.*?)\((\d*)\): col: (\d*) Error: (.*).*/);
			if (syntaxMatch) {
				var pathStr:String = syntaxMatch[1];
				var lineNum:int = syntaxMatch[2];
				var colNum:int = syntaxMatch[3];
				var errorStr:String = syntaxMatch[4];
				pathStr = pathStr.substr(pathStr.lastIndexOf("/")+1);
				errors += HtmlFormatter.sprintf("%s<weak>:</weak>%s \t %s\n",
												pathStr, lineNum, errorStr); 
			}
			
			generalMatch = data.match(/(.*?): Error: (.*).*/);
			if (!syntaxMatch && generalMatch)
			{ 
				pathStr = generalMatch[1];
				errorStr  = generalMatch[2];
				pathStr = pathStr.substr(pathStr.lastIndexOf("/")+1);
				
				errors += HtmlFormatter.sprintf("%s: %s", pathStr, errorStr);
			}
			
			
			ConsoleUtil.addErrorMessage(data);
		}
		
		private function shellExit(e:NativeProcessExitEvent):void {
			ConsoleUtil.print("FCSH exit code "+e.exitCode);
			reset();
			if (exiting) {
				exiting = false;
				startShell();
			}
		}
		
		protected function compilerWarning(...msg):void 
		{
			var text:String = msg.join(" ");
			var textLines:Array = text.split("\n");
			var lines:Vector.<TextLineModel> = Vector.<TextLineModel>([]);
			for (var i:int = 0; i < textLines.length; i++)
			{
				if (textLines[i] == "") continue;
				text = "<warning> ⚠  </warning>" + textLines[i]; 
				var lineModel:TextLineModel = new MarkupTextLineModel(text);
				lines.push(lineModel);
			}
			outputMsg(lines);
		}
		
		protected function compilerError(...msg):void 
		{
			var text:String = msg.join(" ");
			var textLines:Array = text.split("\n");
			var lines:Vector.<TextLineModel> = Vector.<TextLineModel>([]);
			for (var i:int = 0; i < textLines.length; i++)
			{
				if (textLines[i] == "") continue;
				text = "<error> ⚡  </error>" + textLines[i]; 
				var lineModel:TextLineModel = new MarkupTextLineModel(text);
				lines.push(lineModel);
			}
			outputMsg(lines);
		}

	}
}